/**
 * @(#)TraductorBingTranslate 1/11/11
 *
 * Copyright (c) 2005-2010 PsiqueWare S.A. de C.V. All Rights Reserved.
 * This software is the proprietary information PsiqueWare S.A. de C.V.
 * Use is subject to license terms.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *
 * <br/>Usuario : AngelElectrico
 * <br/>Creaci&oacute;n:1/11/11 - 07:40 PM PM
 * <br/>
 * <br/>
 * <br/><t5>Cambios :</t5>
 * <br/><b>Fecha | Autor  |  Cambios</b>
 * <ul>
 * <li>1/11/11 | <a href = "mailto:AngelElectrico@PsiqueWare.com">&Aacute;ngel El&eacute;ctrico</a> | Creaci&oacute;n del Objeto.</li>
 *
 * </ul>
 */

package com.psiqueware.alheli.i18n.traductor.bing;

import com.psiqueware.alheli.exception.NullArgumentException;
import com.psiqueware.alheli.i18n.EscuchaTraductor;
import com.psiqueware.alheli.i18n.Lenguaje;
import com.psiqueware.alheli.i18n.TraduccionException;
import com.psiqueware.alheli.i18n.Traductor;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

import static com.psiqueware.alheli.cripto.CodificadorEnBase64.decodificar;


/**
 * {@link com.psiqueware.alheli.i18n.Traductor} basado en los servicios de Bing translate.
 * <p/>
 * <br/>Paquete Original: com.psiqueware.alheli.i18n.traductor.bing
 * <br/>Nombre  Original: TraductorBingTranslate
 * <br/>Oprima para ver : <a href="TraductorBingTranslate.java.html"><i>C&oacute;digo Fuente</i> </a>
 *
 * @author <a href="mailto:AngelElectrico@PsiqueWare.com">&Aacute;ngel El&eacute;ctrico</a>
 * @version Revision: 1.1 1/11/11 07:40 PM
 */
public final class TraductorBingTranslate implements Traductor
{

    /**
     * {@link EscuchaTraductor} para notificar de los eventos.
     */
    private EscuchaTraductor m_escucha;

    private final static String TF = decodificar("OEI4MUExRUVFMDAyRjU2OUZBQUJEMDJCRThBMjJENTA2NkMxNEVCMD==");

    /**
     * Constructor de TraductorBingTranslate.
     */
    public TraductorBingTranslate()
    {

    }

    /**
     * Construye una TraductorBingTranslate.
     *
     * @param escucha para los eventos de traducci&oacute;n.
     */
    public TraductorBingTranslate(final EscuchaTraductor escucha)
    {
        m_escucha = escucha;
    }

    /**
     * Indica si un {@link com.psiqueware.alheli.i18n.Lenguaje} es soportado por el {@link com.psiqueware.alheli.i18n.Traductor}.
     *
     * @param lenguaje a evaluar.
     *
     * @return <code>true</code> si y solo si el {@link com.psiqueware.alheli.i18n.Traductor} puede traducir texto desde y hacia este {@link com.psiqueware.alheli.i18n.Lenguaje}; <code>false</code> en caso contrario.
     */
    @Override
    public boolean isSoportado(final Lenguaje lenguaje)
    {
        return lenguaje.isBing();
    }

    /**
     * Traduce un texto de un idioma a otro.
     *
     * @param texto         a traducir.
     * @param idiomaOrigen  {@link com.psiqueware.alheli.i18n.Lenguaje} del idioma del texto.
     * @param idiomaDestino {@link com.psiqueware.alheli.i18n.Lenguaje} del idioma deseado para el texto.
     *
     * @return el texto traducido; <code>null</code> si no se puede realizar la traducci&oacute;n.
     */
    @Override
    public String traducir(final String texto, final Lenguaje idiomaOrigen, final Lenguaje idiomaDestino)
    {
        if (null == texto) throw new NullArgumentException("<error>El argumento 'texto' no puede ser NULL.</error>");
        if (0 == texto.trim().length())
            throw new IllegalArgumentException("<error>El argumento 'texto' es una cadena vacia.</error>");
        if (!idiomaOrigen.isBing())
            throw new IllegalArgumentException("<error>El idioma de origen '" + idiomaOrigen.name() + "' no es compatible con este traductor.</error>");
        if (!idiomaDestino.isBing())
            throw new IllegalArgumentException("<error>El idioma destino '" + idiomaDestino.name() + "' no es compatible con este traductor.</error>");

        if (texto.length() > 5200 && texto.contains(">"))
        {
            final String[] porcionesStr = texto.split(">");
            final List<String> porcionesCompletas = new ArrayList<String>();
            StringBuilder sb = new StringBuilder(5000);
            for (int i = 0; i < porcionesStr.length; i++)
            {
                final String porcion = porcionesStr[i] + (i < porcionesStr.length - 1 ? ">" : "");
                sb.append(porcion);
                if (sb.length() >= 5000)
                {
                    porcionesCompletas.add(traduccionDirecta(sb.toString(), idiomaOrigen, idiomaDestino));
                    sb = new StringBuilder(5000);
                }
            }
            if (sb.length() > 0)
            {
                porcionesCompletas.add(traduccionDirecta(sb.toString(), idiomaOrigen, idiomaDestino));
            }

            sb = new StringBuilder();
            for (final String porcionTraducida : porcionesCompletas)
            {
                sb.append(porcionTraducida);
            }
            return verificarEtiquetasIMG(sb.toString());

        }
        else
        {
            return traduccionDirecta(texto, idiomaOrigen, idiomaDestino);
        }
    }

    /**
     * Esta operaci&oacute;n es para darle la vuelta a un problema donde el traductor
     * reemplaza los s&iacute;mbolos de mayor y menor que &lt; y &gt; en las etiquetas
     * 'img'.
     *
     * @param HTML
     *
     * @return El HTML depurado de este error.
     */
    private String verificarEtiquetasIMG(final String HTML)
    {

        if (!HTML.contains("&lt; img src")) return HTML;

        final Pattern pattern = Pattern.compile("&lt; img src", Pattern.LITERAL);
        final String[] strings = pattern.split(HTML);
        final StringBuilder sb = new StringBuilder(HTML.length());
        for (int i = 0; i < strings.length; i++)
        {
            final String string = strings[i];
            final int indexMayorQue = string.indexOf("/ &gt;");
            if (-1 != indexMayorQue)
            {
                sb.append("<img src");
                sb.append(string.substring(0, indexMayorQue));
                sb.append("/>");
                sb.append(string.substring(indexMayorQue + 6, string.length()));
            }
            else if (i == 0)
            {
                sb.append(string);
            }
            else
            {
                sb.append("&lt; img src").append(string);
            }
        }
        return sb.toString();
    }

    private String traduccionDirecta(final String texto, final Lenguaje idiomaOrigen, final Lenguaje idiomaDestino)
    {
        if (10240 < texto.length())
            throw new IllegalArgumentException("<error>El argumento 'texto' no puede ser mayor a 10240 caracteres.</error>");

        String traducido = null;
        if (null != m_escucha) m_escucha.iniciaTraduccion(texto);
        try
        {

            final List<String> traducciones = traduceArreglo(Arrays.asList(texto), idiomaOrigen, idiomaDestino);
            traducido = traducciones.isEmpty() ? "" : traducciones.get(0);
            return verificarEtiquetasIMG(traducido);
        }
        catch (Exception e)
        {
            throw new TraduccionException(e);
        }
        finally
        {
            if (null != m_escucha) m_escucha.finalizaTraduccion(traducido);
        }
    }

    /**
     * Traduce varios textos de un idioma a otro.
     *
     * @param textos        a traducir.
     * @param idiomaOrigen  {@link com.psiqueware.alheli.i18n.Lenguaje} del idioma del texto.
     * @param idiomaDestino {@link com.psiqueware.alheli.i18n.Lenguaje} del idioma deseado para el texto.
     *
     * @return el texto traducido; <code>null</code> si no se puede realizar la traducci&oacute;n.
     */
    @Override
    public List<String> traducir(final List<String> textos, final Lenguaje idiomaOrigen, final Lenguaje idiomaDestino)
    {
        if (!idiomaOrigen.isBing())
            throw new IllegalArgumentException("<error>El idioma de origen '" + idiomaOrigen.name() + "' no es compatible con este traductor.</error>");
        if (!idiomaDestino.isBing())
            throw new IllegalArgumentException("<error>El idioma destino '" + idiomaDestino.name() + "' no es compatible con este traductor.</error>");


        if (null != m_escucha) m_escucha.iniciaTraducciones(textos);
        List<String> traducciones = null;
        try
        {
            traducciones = traduceArreglo(textos, idiomaOrigen, idiomaDestino);
            return traducciones;
        }
        catch (Exception e)
        {
            throw new TraduccionException(e);
        }
        finally
        {
            if (null != m_escucha) m_escucha.finalizaTraducciones(traducciones);
        }
    }

    /**
     * Genera el documento con la petici&oacute;n XML a realizar.
     *
     * @param textos        a traducir.
     * @param idiomaOrigen  {@link com.psiqueware.alheli.i18n.Lenguaje} del idioma del texto.
     * @param idiomaDestino {@link com.psiqueware.alheli.i18n.Lenguaje} del idioma deseado para el texto.
     *
     * @return el {@link Document} generado.
     *
     * @throws Exception en caso de un error.
     */
    private Document generaXmlPeticion(List<String> textos, Lenguaje idiomaOrigen, Lenguaje idiomaDestino) throws Exception
    {

        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        final DocumentBuilder documentBuilder = factory.newDocumentBuilder();
        final Document documento = documentBuilder.newDocument();

        final Element raiz = documento.createElement("TranslateArrayRequest");
        documento.appendChild(raiz);

        final Element AI = documento.createElement("AppId");
        AI.appendChild(documento.createTextNode(TF));
        raiz.appendChild(AI);

        final Element desde = documento.createElement("From");
        desde.appendChild(documento.createTextNode(idiomaOrigen.getIso()));
        raiz.appendChild(desde);

        final Element opciones = documento.createElement("Options");
        final Element categoria = documento.createElement("Category");
        categoria.setAttribute("xmlns", "http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2");
        opciones.appendChild(categoria);

        final Element tipoContenido = documento.createElement("ContentType");
        tipoContenido.setAttribute("xmlns", "http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2");
        tipoContenido.appendChild(documento.createTextNode("text/plain"));
        opciones.appendChild(tipoContenido);

        final Element banderasReservadas = documento.createElement("ReservedFlags");
        banderasReservadas.setAttribute("xmlns", "http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2");
        opciones.appendChild(banderasReservadas);

        final Element estado = documento.createElement("State");
        estado.setAttribute("xmlns", "http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2");
        opciones.appendChild(estado);

        final Element uri = documento.createElement("Uri");
        uri.setAttribute("xmlns", "http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2");
        opciones.appendChild(uri);

        final Element usuario = documento.createElement("User");
        usuario.setAttribute("xmlns", "http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2");
        opciones.appendChild(usuario);

        raiz.appendChild(opciones);

        final Element textosATraducir = documento.createElement("Texts");
        for (final String texto : textos)
        {
            final Element hijo = documento.createElement("string");
            hijo.setAttribute("xmlns", "http://schemas.microsoft.com/2003/10/Serialization/Arrays");
            hijo.appendChild(documento.createTextNode(texto));
            textosATraducir.appendChild(hijo);
        }
        raiz.appendChild(textosATraducir);

        final Element destino = documento.createElement("To");
        destino.appendChild(documento.createTextNode(idiomaDestino.getIso()));
        raiz.appendChild(destino);

        return documento;
    }

    /**
     * Transforma el {@link Document} en el XML a postear.
     *
     * @param documento a publicar.
     *
     * @return cadena con el texto del documento.
     *
     * @throws TransformerException en caso de un error.
     */
    private String transformar(final Document documento) throws TransformerException
    {
        final TransformerFactory fabricaTransformadores = TransformerFactory.newInstance();
        Transformer transformador = fabricaTransformadores.newTransformer();
        transformador.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        transformador.setOutputProperty(OutputKeys.INDENT, "yes");

        // create string from xml tree
        final StringWriter sw = new StringWriter();
        final StreamResult result = new StreamResult(sw);
        final DOMSource source = new DOMSource(documento);
        transformador.transform(source, result);
        return sw.toString();
    }

    /**
     * Ejecuta la petici&oacute;n al servidor de bing.
     *
     * @param xml petici&oacute;n a ejecutar.
     *
     * @return xml respuesta.
     *
     * @throws Exception en caso de un error.
     */
    private static String ejecutaPeticion(String xml) throws Exception
    {
        DefaultHttpClient httpclient = new DefaultHttpClient();
        String uri = "http://api.microsofttranslator.com/v2/Http.svc/TranslateArray";
        HttpPost post = new HttpPost(uri);

        StringEntity reqEntity = new StringEntity(xml, "utf-8");
        reqEntity.setContentType("application/xml");

        post.setEntity(reqEntity);
        HttpResponse response = httpclient.execute(post);
        HttpEntity resEntity = response.getEntity();
        return EntityUtils.toString(resEntity);
    }


    /**
     * Devuelve el listado de cadenas traducidas.
     *
     * @param xml a traducir.
     *
     * @return {@link List} de {@link String} traducidas.
     *
     * @throws Exception en caso de un error.
     */
    private static List<String> traduceRespuestaXML(String xml) throws Exception
    {
        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        final DocumentBuilder constructor = factory.newDocumentBuilder();

        final InputSource inputSource = new InputSource(new StringReader(xml));
        Document documento = constructor.parse(inputSource);

        documento.getDocumentElement().normalize();

        final List<String> salida = new ArrayList<String>();
        NodeList nodos = documento.getElementsByTagName("TranslatedText");
        for (int i = 0; i < nodos.getLength(); i++)
        {
            salida.add(nodos.item(i).getTextContent());
        }

        return salida;
    }

    /**
     * Traduce un listado.
     *
     * @param textos  a traducir.
     * @param origen  {@link Lenguaje} de las cadenas en el arreglo textos.
     * @param destino {@link Lenguaje} de las cadenas en el arreglo destino.
     *
     * @return {@link List} de {@link String} a devolver.
     *
     * @throws Exception a mostrar.
     */
    public List<String> traduceArreglo(List<String> textos, Lenguaje origen, Lenguaje destino) throws Exception
    {
        for (String s : textos)
        {
            if (s.length() > 10240)
            {
                throw new RuntimeException("Each string to be translated must be 10,240 characters or less.");
            }
        }

        final Document documento = generaXmlPeticion(textos, origen, destino);
        final String xml = transformar(documento);
        final String respuesta = ejecutaPeticion(xml);
        return traduceRespuestaXML(respuesta);

    }

    public static void main(String[] args)
    {
        final TraductorBingTranslate traductor = new TraductorBingTranslate();
        final String traduccion = traductor.traducir("Hola Mundo", Lenguaje.SPANISH, Lenguaje.ENGLISH);
        System.out.println(traduccion);
    }
}

